package com.appspot.bibtable.servlet;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.jdo.Extent;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import com.appspot.bibtable.model.ReferenceEntityIndex;
import com.appspot.bibtable.model.UserInfo;
import com.appspot.bibtable.util.StringUtility;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

final class QueryBuilder<T>
{
	private static final Logger logger = Logger.getLogger(QueryBuilder.class.getName());

	private static final Pattern anyPattern = Pattern.compile("(?:(?=^.)|(?<=\\s))(?:(\\w+)(:|<=|>=|<|>))?+(\"?+)([^\":]+?)\\3(?:(?=\\s)|(?<=.$))", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);

	private final PersistenceManager manager;
	private final Query query;
	private final Map<String, Object> parameters;

	public QueryBuilder(PersistenceManager manager, Class<T> c)
	{
		this(manager, manager.getExtent(c));
	}

	public QueryBuilder(PersistenceManager manager, Extent<T> extent)
	{
		this.query = manager.newQuery(extent);
		this.query.setRange(0, 50);
		this.manager = manager;
		parameters = new HashMap<String, Object>();
	}

	public List<T> executeQuery()
	{
		StringBuilder declarations = new StringBuilder();
		for (String name : parameters.keySet())
		{
			Object parameter = parameters.get(name);
			Class<?> c = parameter.getClass();
			declarations.append(c.getName() + " " + name + ", ");
		}
		if (declarations.length() > 0)
		{
			int length = declarations.length();
			declarations.delete(length - 2, length);
			query.declareParameters(declarations.toString());
		}

		return (List<T>)query.executeWithMap(parameters);
	}

	void setFilter(String filter)
	{
		if (StringUtility.isNullOrEmpty(filter))
		{
			return;
		}

		try
		{
			filter = URLDecoder.decode(filter, "UTF-8");
		}
		catch (UnsupportedEncodingException ex)
		{
			logger.warning(ex.getMessage());
		}

		StringBuilder filterBuilder = new StringBuilder();
		Matcher matcher = anyPattern.matcher(filter);
		while (matcher.find())
		{
			String key = matcher.group(1);
			if (key != null)
			{
				key = key.toLowerCase();
			}
			String operator = matcher.group(2);
			String token = matcher.group(4);

			if ("type".equals(key))
			{
				buildTypeFilter(operator, token, filterBuilder);
			}
			else if ("author".equals(key))
			{
				buildAuthorFilter(operator, token, filterBuilder);
			}
			else if ("tag".equals(key))
			{
				buildTagFilter(operator, token, filterBuilder);
			}
			else if ("published".equals(key))
			{
				buildPublishedFilter(operator, token, filterBuilder);
			}
			else if ("bookmark".equals(key))
			{
				buildBookmarkFilter(operator, token, filterBuilder);
			}
			else
			{
				if (key != null && !"title".equals(key))
				{
					logger.warning("Unknown key (" + key + ") was given.");
				}
				buildTitleFilter(operator, token, filterBuilder);
			}
		}

		final int filterLength = filterBuilder.length();
		if (filterLength > 0)
		{
			query.setFilter(filterBuilder.delete(filterLength - 4, filterLength).toString());
		}
	}

	private void buildTypeFilter(String operator, String token, StringBuilder filterBuilder)
	{
		filterBuilder.append("type == '" + token + "' && ");
	}

	private void buildAuthorFilter(String operator, String token, StringBuilder filterBuilder)
	{
		filterBuilder.append("author.contains('" + token + "') && ");
	}

	private void buildTagFilter(String operator, String token, StringBuilder filterBuilder)
	{
		filterBuilder.append("tag.contains('" + token + "') && ");
	}

	private void buildPublishedFilter(String operator, String token, StringBuilder filterBuilder)
	{
		DateFormat format = new SimpleDateFormat("yyyy");
		try
		{
			Date date = format.parse(token);

			String parameter = null;
			if (operator.startsWith(">"))
			{
				parameter = "lower";
			}
			else if (operator.startsWith("<"))
			{
				parameter = "upper";
			}
			else
			{
				operator = "==";
				parameter = "exact";
			}

			filterBuilder.append("published " + operator + " " + parameter + " && ");
			parameters.put(parameter, date);
		}
		catch (ParseException ex)
		{
			logger.warning("Unkown date format.");
		}
	}
	
	private void buildBookmarkFilter(String operator, String token, StringBuilder filterBuilder)
	{
		UserService service = UserServiceFactory.getUserService();
		UserInfo userInfo = UserInfo.getUserInfoOf(service.getCurrentUser(), manager);
		if (userInfo != null)
		{
			Set<Key> bookmark = userInfo.getBookmarks();
			if (bookmark != null && bookmark.size() > 0)
			{
				filterBuilder.append("bookmark.contains(key)" + " && ");
				parameters.put("bookmark", new ArrayList<Key>(bookmark));
			}
			else
			{
				logger.warning("No bookmark. Request is ignored.");
			}
		}
		else
		{
			logger.warning("Request bookmark filter from non-registered user.");
		}
	}

	private void buildTitleFilter(String operator, String token, StringBuilder filterBuilder)
	{
		String parameter = getNewParameter();
		filterBuilder.append("term == " + parameter + " && ");
		parameters.put(parameter, ReferenceEntityIndex.getTokens(token, ReferenceEntityIndex.MAXIMUM_NUMBER_OF_WORDS_TO_SEARCH));
	}

	private String getNewParameter()
	{
		return "_" + parameters.size();
	}

	void setSort(String sort)
	{
		if (StringUtility.isNullOrEmpty(sort))
		{
			return;
		}

		String[] orders = sort.split(",");
		StringBuilder builder = new StringBuilder();
		for (int index = 0; index < orders.length; index++)
		{
			String order = orders[index];
			if (order.startsWith("-"))
			{
				builder.append(order.substring(1) + " desc");
			}
			else
			{
				builder.append(order + " asc");
			}
			if (index < orders.length - 1)
			{
				builder.append(", ");
			}
		}
		query.setOrdering(builder.toString());
	}

	void setRange(String start, int displayCount)
	{
		if (start == null)
		{
			return;
		}

		long startIndex = 0;
		try
		{
			startIndex = Long.parseLong(start);
			if (startIndex < 0)
			{
				logger.warning("Negative 'start' was given.");
				startIndex = 0;
			}
		}
		catch (NumberFormatException ex)
		{
			logger.warning(ex.getMessage());
		}
		query.setRange(startIndex, startIndex + displayCount);
	}

	String getQueryString()
	{
		return query.toString();
	}
	
	Map<String, Object> getParameters()
	{
		return Collections.unmodifiableMap(parameters);
	}

	static String removeParameterFromPathAndQuery(String path, String parameterName)
	{
		if (path == null)
		{
			return null;
		}

		int queryIndex = path.indexOf('?');
		if (queryIndex < 0)
		{
			return path;
		}

		String requestUri = path.substring(0, queryIndex);
		String pattern = "(&" + parameterName + "=[^&]*|" + parameterName + "=[^&]*&)";
		String queryString = path.substring(queryIndex + 1).replaceAll(pattern, "");
		return requestUri + "?" + queryString;
	}
}
